/*
    This file is part of QTau
    Copyright (C) 2013-2018  Tobias "Tomoko" Platen <tplaten@posteo.de>
    Copyright (C) 2013       digited       <https://github.com/digited>
    Copyright (C) 2010-2013  HAL@ShurabaP  <https://github.com/haruneko>

    QTau is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    SPDX-License-Identifier: GPL-3.0+
*/

#include "mainwindow.h"

#include "Session.h"
#include "Controller.h"
#include "PluginInterfaces.h"
#include "Utils.h"
#include <QJsonDocument>

#include "audio/jackaudio.h"
#include "audio/audioengine.h"

#include <QApplication>
#include <QPluginLoader>
#include <QDebug>
#include <QTimer>
#include <qsettings.h>

#define __devloglevel__ 4


qtauController::qtauController(QString dir,QObject *parent) :
    QObject(parent), _jack(nullptr), _mainWindow(nullptr), _activeSession(nullptr)
{
    QDir diru(dir);
    diru.cdUp();
    _prefix = diru.absolutePath();


    QSettings settings;
    bool autoconnect = settings.value("jackAutoConnect").toBool();
    autoconnect = true;//FIXME: hack until we have setting panel
    _jack = new JackAudio(autoconnect);
    _audio = new AudioEngine(_jack,this);
    _audio->setUseJackTransport(true);
    _outbuf = new OutputBuffer(_jack);
    _audio->setOutputBuffer(_outbuf);
    _jackSampleRate=_jack->getSamplerate();

    connect(_outbuf,&OutputBuffer::startPlayback,this,&qtauController::outbuf_startPlayback);
    connect(_outbuf,&OutputBuffer::stopPlayback,this,&qtauController::outbuf_stopPlayback);


    QTimer *timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(jackTimer()));
    timer->start(10);

    _activeSynth = nullptr;
    _selectedSynth = nullptr;

    float bpm=120; //TODO hardcoded in editor, change settings on UST load
    int numerator=4;
    int denominator=4;
    _samplesToMeasures = (bpm*numerator)/(denominator*_jackSampleRate*60);

    setupTranslations();
    setupPlugins();
}
int qtauController::sampleRate()
{
    return _jack->sampleRate();
}

void qtauController::startOfflinePlayback(const QString &fileName)
{
    _outbuf->openReadFile(fileName);
    _lastPlay = fileName;
    startPlayback(0);
}

void qtauController::jackTimer()
{
    char midi[1024];
    if(_jack->readMidiData(midi,1024))
    {
        int event_type = midi[0] & 0xF0;
        int note_num = midi[1];
        if(event_type==144)
        {
            pianoKeyPressed(note_num);
        }
        else if(event_type==128)
        {
            pianoKeyReleased(note_num);
        }
    }

    if(_jack->stateChanged())
    {
        switch(_jack->transportState())
        {
        case JackTransportStopped:
            DEVLOG_DEBUG("state_changed to JackTransportStopped");
            //XXX if(_activeSynth) _activeSynth->stopThread();
            transportPositionChanged(-1);
            break;
        case JackTransportStarting:
            DEVLOG_DEBUG("state_changed to JackTransportStarting");
            break;
        case JackTransportLooping:
            DEVLOG_DEBUG("not supported JackTransportLooping");
            break;
#ifdef TRISQUEL7
        case JackTransportNetStarting:
            DEVLOG_DEBUG("not supported JackTransportNetStarting");
#endif
        default:
            break;
        }
    }

    int pos = _jack->positionChange();

    if(_nonzeroStart>0) {
        pos = -1;
        _nonzeroStart--;
        DEVLOG_DEBUG("NONZERO START "+STR(_nonzeroStart));
    }

    if(pos!=-1 && _audio->transportPosition()!=pos)
    {
        _audio->setTransportPosition(pos);
        //XXX if(_activeSynth) _activeSynth->stopThread();
        _localRequestStartPlayback = false;
    }

    if(_jack->isRolling() || (_audio->useJackTransport()==false && _audio->localTransportRolling())) transportPositionChanged(_samplesToMeasures*_audio->transportPosition());

}

void qtauController::outbuf_startPlayback()
{
   // startPlayback(0);
   DEVLOG_DEBUG("playback is stable");
}

void qtauController::outbuf_stopPlayback()
{
    stopPlayback();
}


qtauController::~qtauController()
{
    delete _jack;
    //TODO --> shutdown audio -> crash here
    delete _audio;
    delete _mainWindow;
}



//------------------------------------------

static qtauController* singleton=0;

void qtauController::shutdown(int rc)
{
    (void) rc;
    _jack->shutdown();
    _audio->shutdown();//kind of CopyEngine
}

bool qtauController::run()
{
    _mainWindow = new MainWindow();
    _mainWindow->show();

    newEmptySession();
    _mainWindow->setController(*this, *this->_activeSession);

    singleton = this;

    return true;
}

qtauController* qtauController::instance()
{
    return singleton;
}


bool qtauController::setupTranslations()
{
    //FIXME: english only
    return false;
}


bool qtauController::setupPlugins()
{
    //FIXME plugins dir, should not be hardcoded
    _pluginsDir = QDir(qApp->applicationDirPath());

    if(_pluginsDir.cd("plugins")==false) {
        _pluginsDir = QDir(_prefix+"/lib/qtau/");
        if(_pluginsDir.cd("plugins")==false) return false;
    }


    foreach (QString fileName, _pluginsDir.entryList(QDir::Files))
    {


        QPluginLoader loader(_pluginsDir.absoluteFilePath(fileName));
        QObject *plugin = loader.instance();

        if (plugin)
        {
            ISynth* s = qobject_cast<ISynth*>(plugin);

            if (s)
                initSynth(s);
            else
            {
                IPreviewSynth* ps = qobject_cast<IPreviewSynth*>(plugin);
                if(ps)
                {
                    initPreviewSynth(ps);
                }
            }



        }
        else DEVLOG_INFO("Incompatible plugin: " + fileName+ "reason: "+loader.errorString());
    }

    return false;
}

void qtauController::initPreviewSynth(IPreviewSynth *ps)
{
    ps->setup(this);
    _audio->setPreviewSynth(ps);
    _preview=ps;
}

void qtauController::initSynth(ISynth *s)
{
    if (!_synths.contains(s->name()))
    {
        s->setup(this);
        DEVLOG_INFO("Adding synthesizer " + s->name());
        _synths[s->name()] = s;
        _voices.append(s->listVoices());
    }
    else DEVLOG_INFO("Synthesizer " + s->name() + " is already registered!");
}

void qtauController::selectSinger(QString singerName)
{
    foreach (QString synthName, _synths.keys()) {
        if(_synths[synthName]->setVoice(singerName))
        {
            DEVLOG_DEBUG("setSynth: "+synthName)
                    _selectedSynth = _synths[synthName];
        }
    }
}

void qtauController::newEmptySession()
{
    _activeSession = new qtauSession(this);
}



//------------------------------------------

void qtauController::addFileToRecentFiles(QString fileName)
{
    DEVLOG_DEBUG("addFileToRecentFiles: "+fileName);

    QSettings settings;
    QStringList files = settings.value("recentFileList").toStringList();
    files.removeAll(fileName);
    files.prepend(fileName);
    while (files.size() > MAXRECENTFILES)
        files.removeLast();

    foreach(QString file, files)
        DEVLOG_DEBUG("recent: "+file);

    settings.setValue("recentFileList", files);

    _mainWindow->updateRecentFileActions();
}


void qtauController::onLoadUST(QString fileName)
{
    if (!fileName.isEmpty())
    {
        if (!_activeSession)
            newEmptySession();
        _activeSession->loadUST(fileName);
        addFileToRecentFiles(fileName);

    }
    else DEVLOG_WARNING("empty UST file name");
}

void qtauController::onSaveUST(QString fileName, bool rewrite)
{
    if (_activeSession && !_activeSession->isSessionEmpty())
    {

        QFile uf(fileName);

        if (uf.open(QFile::WriteOnly))
        {
            addFileToRecentFiles(fileName);
            if (uf.size() == 0 || rewrite)
            {
                uf.reset(); // maybe it's redundant?..
                QJsonArray array;
                _activeSession->ustJson(array);
                QJsonDocument doc(array);
                uf.write(doc.toJson());
                uf.close();

                _activeSession->setFilePath(fileName);
                _activeSession->setSaved();

                DEVLOG_DEBUG("UST saved to " + fileName);
            }
            else DEVLOG_ERROR("File " + fileName + " is not empty, rewriting cancelled");
        }
        else DEVLOG_ERROR("Could not open file " + fileName + " to save UST");
    }
    else DEVLOG_ERROR("Trying to save ust from empty session!");
}


//new synth api required (madde)
void qtauController::pianoKeyPressed(int keyNum)
{
    _previewRunning=true;
    if(_preview) _preview->start(keyNum);
}

void qtauController::pianoKeyReleased(int keyNum)
{
    DEVLOG_DEBUG("piano key released "+STR(keyNum));
    _previewRunning=false;
    if(_preview) _preview->stop();
}

bool qtauController::validateScore(const QJsonArray& ust)
{
    // struct selectionRange sel  = _mainWindow->getSelectionRange();
    //XXX if(_activeSynth) _activeSynth->setPlaybackSelection(sel);

    int tempo = 0;
    int lastNoteEnd=0;
    int noteCount = 0;
    for (int i = 0; i < ust.count(); ++i)
    {
        auto o = ust[i].toObject();

        if(!o.contains(NOTE_KEY_NUMBER)) {
            QJsonDocument doc(o);
            tempo=o[TEMPO].toInt();
            continue;
        }
        noteCount++;


        int noteOffset = o[NOTE_PULSE_OFFSET].toInt();
        int noteLength = o[NOTE_PULSE_LENGTH].toInt();
        int notenum = o[NOTE_KEY_NUMBER].toInt();
        QString lyric = o[NOTE_LYRIC].toString();

        int rest = noteOffset-lastNoteEnd;

        if(rest<0)
        {
            DEVLOG_ERROR("overlapping notes "+STR(i)+" "+STR(i-1));
            //SET color to red
            auto o = ust[i].toObject();
            auto o2 = ust[i-1].toObject();
            _mainWindow->markOverlappingNotes(_activeSession->getNote(o),_activeSession->getNote(o2));
            return false;
        }

        if(notenum<0 && notenum>127) return false;

        lastNoteEnd = noteOffset+noteLength;
    }
    if(noteCount==0) return false; //empty score
    if(tempo==0) return false;
    _mainWindow->updateNoteColors();
    return true;
}

void qtauController::onRequestStartPlayback()
{
    _localRequestStartPlayback=true;
    QJsonArray ust;
    _activeSession->ustJson(ust);
    foreach (QString synthName, _synths.keys()) {
        //XXX _synths[synthName]->stopThread();
        //only one synth can be scheduled
    }
    if(_activeSynth!=_selectedSynth)
    {
        //XXX if(_activeSynth) _activeSynth->stopThread();
        _activeSynth = _selectedSynth;
        DEVLOG_DEBUG("synth selection");
    }
    if(validateScore(ust))
    {
        //if(!_synthrunning)
        {
            QJsonArray ust;

            _activeSession->ustJson(ust);

            int noteCount=0;

            for (int i = 0; i < ust.count(); ++i)
            {
                auto o = ust[i].toObject();

                if(!o.contains(NOTE_KEY_NUMBER)) {
                    continue;
                }

                noteCount++;
            }

            if(noteCount==0)
            {
                _mainWindow->onLog("empty session - nothing to do",ELog::error);
                _jack->changeTranportState(TRANSPORT_STOP);
            }
            else
            {

                _activeSynth->setScore(ust);
                DEVLOG_ERROR("synth start set seek\n");

                if(_audio->useJackTransport()) _jack->changeTranportState(TRANSPORT_STOP);
                else _audio->setLocalTransportRolling(false);

                QString tmp = _activeSession->documentFile();
                tmp.replace(".ustj",".cache");
                if(tmp.length()==0) tmp="/tmp";
                _activeSynth->setCacheDir(tmp);
                bool ready = _activeSynth->synthesize();//start synth

                if(ready)
                {
                    if(_activeSynth->synthIsRealtime())
                    {
                        DEVLOG_DEBUG("schedule synth");
                        _outbuf->scheduleSynth(_activeSynth);
                        startPlayback(0);
                    }
                    else
                    {
                        DEVLOG_DEBUG("TODO:: synth must start playback");
                        _mainWindow->onLog("synth running offline",ELog::info);
                    }
                }
            }
        }
    }
}

void qtauController::onRequestStopPlayback()
{
    _localRequestStartPlayback = false;
    if(_audio->useJackTransport()) _jack->changeTranportState(TRANSPORT_STOP);
    else {
        _audio->setLocalTransportRolling(false);
        //XXX if(_activeSynth) _activeSynth->stopThread();
    }
}

void qtauController::onRequestResetPlayback()
{
    _localRequestStartPlayback = false;
    // XXX if(_activeSynth) _activeSynth->stopThread();

    _audio->setTransportPosition(0);
    if(_audio->useJackTransport())
    {
        _jack->changeTranportState(TRANSPORT_ZERO);
    }

}


//from synth plugin
void qtauController::startPlayback(float startPos)
{
    //_synthrunning = true;
    _mainWindow->onLog("start playback",ELog::info);
    if(_audio->useJackTransport()==false)
    {
        _audio->setLocalTransportRolling(true);
        float _sampleRate_ = _jack->sampleRate(); //FIXXME
        if(startPos) _audio->setTransportPosition(_sampleRate_*startPos);
    }
    else
    {

        if(startPos==0)
        {
            _nonzeroStart=0;
            _jack->changeTranportState(TRANSPORT_START);
        }
        else
        {
            _nonzeroStart=30;
            float _sampleRate_ = _jack->sampleRate();
            _audio->setTransportPosition(_sampleRate_*startPos);
            _jack->transportStartPos(startPos);

        }
    }



}

void qtauController::stopPlayback()
{
    _localRequestStartPlayback=false;
    //_synthrunning = false;
    _mainWindow->onLog("stop playback",ELog::info);
    if(_audio->useJackTransport()==false)
    {
        _audio->setLocalTransportRolling(false);
        transportPositionChanged(-1);
    }
    else
        _jack->changeTranportState(TRANSPORT_STOP);
}

void qtauController::setJackTranportEnabled(bool enabled)
{
    if(enabled==false)
        _jack->changeTranportState(TRANSPORT_STOP);

    _audio->setLocalTransportRolling(false);
    _jack->setUseTransport(enabled);
    _audio->setUseJackTransport(enabled);

}

void  qtauController::updateTempoTimeSignature(int tempo)
{
    float bpm=tempo;
    _samplesToMeasures = bpm/(_jackSampleRate*60);
}

void qtauController::logError(const QString &error)
{
    _mainWindow->onLog(error,ELog::error);
}

void qtauController::logDebug(const QString &debug)
{
    _mainWindow->onLog(debug,ELog::debug);
}

void qtauController::logSuccess(const QString &success)
{
    _mainWindow->onLog(success,ELog::success);
}

void qtauController::addPluginAction(QAction *action)
{
    _mainWindow->addPluginAction(action);
}

#if 0
void qtauController::startThread(IThreaded *threaded)
{
    WorkerThread *workerThread = new WorkerThread(threaded);
    workerThread->start();
    connect(workerThread,&WorkerThread::resultReady,workerThread,&WorkerThread::threadEnd);
    connect(workerThread,&WorkerThread::finished,workerThread,&WorkerThread::threadEnd2);
}
#endif

